嗨嗨~ 大家好,我是中義。
這是三十天挑戰的第一天,現在透過 AI 來協助開發已經是常態了,從vibe coder 到資深工程師,透過 AI Agent 來協助開發已經是稀鬆平常的事情,AI 輸出的很快,但他真的安全嗎 ?
一般的資安文章在教你防外面的人,這個系列不是。這三十天要防的,是你每天在用的那些 AI 工具,還有它們幫你寫進專案裡的東西。
那不是同一件事。外面的攻擊者要先找到你,你的 AI 工具已經在你的專案裡了,它幫你寫的 CRUD 沒檢查權限、它推薦的套件可能根本不存在、你裝的那幾個 MCP 拿到了什麼權限你說不出來,這些東西沒有人會幫你看,因為你就是那個要看的人。
三十天結束的時候,你手上會有這些東西:一個不再持有金鑰的前端、一份自己專案的「什麼不能貼給 AI」清單、一組固定的攻擊集、接進 CI 的回歸測試,還有一個跑在你自己筆電上、掃得動自己專案的資安模型。
消耗大約 2GB 記憶體,不用付錢給誰。
全部在你自己的機器上驗證過。不買東西,不等任何人批准。
不是三十個獨立的資安名詞。是跟著你用 AI 開發的整個過程走一遍,從你打開編輯器那一刻,到應用上線之後有人在打它。
金鑰放哪、.env 到底安不安全、外洩了怎麼處置、你貼給 AI 的那段程式碼裡有什麼、它寫回來的東西誰檢查、生出來的程式碼在哪裡跑、它推薦你裝的套件存不存在、你裝的那些 MCP 拿到了什麼權限。
這時候使用者變成陌生人,信任邊界畫在哪、提示注入為什麼防不住、指令藏在模型讀的網頁裡怎麼辦、藏在工具描述裡呢、RAG 知識庫被投毒、防護 prompt 怎麼寫才驗得了、agent 該拿多少權限、AI 生的 CRUD 有沒有檢查授權、從模型講的話到真的執行動作中間該卡什麼、端點被狂打或被當免費 ChatGPT。
你的防線擋下了什麼你其實不知道、一次修補怎麼變成永久的測試、CI 什麼時候該擋住你自己。
打得到的地方在哪、全綠的測試是防住了還是根本沒打到、成績單不能由生它的人打。
把一個筆電跑得動的資安模型架在自己機器上,用 CWE 來掃自己的專案,確認手上那個模型沒被掉包,最後誠實回答一句:你的專案現在防到什麼程度。
這條線上的每一站,都是 AI 進到開發流程之後才長出來的問題。有些看起來像老問題(金鑰、授權、注入),但成因換了,所以處理的順序和驗證的方式也跟著換。
今天從最短的那條開始吧 !
先想一個場景。
你開著 Claude Code 或 Codex,跟它說「幫我串一下這家的模型 API,做個介面可以聊天」。
三十秒之後畫面上出現一個能跑的東西,你按下去,模型回話了,你很爽。
那一刻你檢查了什麼?
大部分人檢查的是「會不會動」。會動就 commit 了。
但那三十秒裡它幫你決定了一件事:那個 API 要從哪裡打出去。
而在沒有其他指示的情況下,它給的答案幾乎都是同一個:從前端打,因為那是最短的路,程式碼最少,最快看到會動。
它沒有錯。你要的是「能跑的介面」,它給了你能跑的介面,成本、責任、金鑰放哪,這些你沒問,它就沒答。
這是這三十天要一直回來的那件事:AI 幫你做了很多決定,而它只回答你問的那個問題。
今天這一題就是它幫你做的第一個決定。
你寫的前端程式碼,最後不是在你的電腦上執行的。
這句話講出來大家都同意。那就是網頁的運作方式:使用者連上你的網站,瀏覽器把你的 JavaScript 下載下來,在他的機器上跑起來。國中生都知道。
但這件事的含意,我認識的開發者裡沒幾個真的想過。
上個月幫朋友看一個側邊專案。他做了一個會叫模型改寫文案的小工具,做得挺好,自己付 API 的錢,一個月幾百塊台幣,還在可以接受的範圍。
我問他金鑰放哪。
「前端啊,不過我有混淆過啦。」
我沒回答,請他按 F12,切到 Network,隨便送一次請求,點開那個打模型的請求,翻到 Headers。
Authorization: Bearer sk-proj-xxxxxxxxxxxxxxxxxxxx
完整的一串明文,就在那裡
他盯著看了三秒,說:「可是我把它拆成三段再拼回去欸。」
這句話我想了一下要怎麼回。後來我說:那個拼回去的動作,是誰的電腦做的?
拆成三段的程式碼,也要下載到使用者的瀏覽器才能執行。要把三段拼起來的那一行,也是在他的機器上跑。拼完之後那個完整的金鑰,要寫進 request header,才發得出去。而那個 header,就在他的 Network 面板裡攤著。
你能寫進程式碼裡的每一個步驟,使用者都看得到。因為那些步驟本來就是在他家執行的。

這張圖之後會一直回來。今天只有兩個框,紅色那格就是今天的問題:金鑰待在右邊,後面幾天我們會在同一張圖上加東西,看它怎麼長成整個系列的信任邊界。
所以不管你把它拆幾段、用什麼函式包幾層、換成什麼奇怪的編碼,最後一定有一個瞬間,完整的金鑰會出現在他的機器上。不然請求發不出去。
反過來說也成立,而且這句話比較好記:瀏覽器帶著 bearer API key 發得出那個請求,就代表那把金鑰到過瀏覽器。
這是這一整天唯一需要記住的句子。三年後你換一套框架、換一家供應商、換一種語言,它還是成立,因為它講的不是某個工具的行為,是誰的機器在跑那段程式碼。
順帶把最容易誤會的那個講掉:VITE_ 或 NEXT_PUBLIC_ 開頭的環境變數,build 完就躺在 bundle 裡,跟寫死在原始碼沒有分別。它叫環境變數沒錯,但它的環境是使用者的瀏覽器。
兩招,五分鐘。
第一招剛才示範過了:DevTools → Network → Fetch/XHR,點那個打模型的請求,看 Headers。
第二招是直接翻 build 出來的東西:
npm run build
grep -rIl "sk-" dist/
sk- 換成你家供應商的金鑰前綴。這個檢查只列出檔名,不會把完整金鑰印進終端紀錄。找得到代表確定洩漏;找不到不代表安全,因為其他前綴、編碼或未走到的請求仍可能漏過。
這兩招等一下還要用,因為改完之後要拿同一組指令回來驗。
手上沒有適合的專案也沒關係,用這個系列的範例專案,它內建了一模一樣的洞。
既然瀏覽器一定會看到它送出去的東西,那答案就只剩一個:別讓瀏覽器碰到金鑰,中間放一台你控制的機器,讓它去跟供應商講話。
import express from "express";
const app = express();
app.use(express.json({ limit: "16kb" }));
app.post("/api/chat", async (req, res) => {
const message = req.body?.message;
if (typeof message !== "string" || message.length === 0 || message.length > 4000) {
return res.status(400).json({ error: "invalid message" });
}
try {
const r = await fetch("https://api.provider.example/v1/chat", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${process.env.PROVIDER_API_KEY}`,
},
signal: AbortSignal.timeout(30_000),
body: JSON.stringify({
model: process.env.PROVIDER_MODEL,
messages: [{ role: "user", content: message }],
}),
});
return res.status(r.status).json(await r.json());
} catch {
return res.status(502).json({ error: "provider unavailable" });
}
});
app.listen(8787);
前端那邊,把原本打供應商的網址改成 /api/chat,只送 { message },金鑰整段刪掉。
先停一下:這段只解決「供應商金鑰不進瀏覽器」,還不能直接上線。
/api/chat目前沒有身分驗證、每人額度與速率限制,公開部署後仍可能被當成免費模型端點。之後會把這些補齊;今天先把信任邊界畫對。

跟上面那張對照著看,差別只有一個:金鑰那格從右邊移到左邊,邊界沒有消失,是你把該留在裡面的東西留在裡面了。
這段骨架可以叫 AI 幫你起稿,但輸入限制、逾時與錯誤路徑仍要自己確認,接下來那一步更不能只問它。
它看不到你的 Network 面板。你問它「這樣安全了嗎」,它只能根據你貼給它的片段回答
「應該沒問題了」。那不是驗證,那是複述。讓 AI 宣告安全,等於沒驗。
重跑剛才那兩招。瀏覽器不該再出現供應商 API key,也不該直接向供應商端點送出請求;應用自己的登入權杖仍可能放在 Authorization,不能只看這個 header 名稱。build 產物裡,grep 也不該再列出含有供應商金鑰前綴的檔案。
我自己第一次做這件事的時候卡在這裡。前端明明改乾淨了,grep 還是一直找得到。
找了快半小時,才發現我在搜舊的 build 產物。dist/ 沒清,我從頭到尾都在看上一版的檔案。
rm -rf dist 再 build 一次就沒了。
這件事講出來有點蠢,但它後面還會再發生好幾次:驗防護 prompt 的時候、驗回歸測試的時候。
驗收的第一個動作,永遠是確認你在看的是新的東西。
有人會問:如果金鑰是使用者自己填的,那前端放著總可以了吧?他的錢他自己負責。
這種模式叫 BYOK,使用者自帶金鑰,在 AI 產品裡很常見。
換成使用者自己的金鑰,風險沒有消失。長效 bearer API key 一旦進入網頁,就會暴露給該頁面執行的程式、第三方腳本、XSS 與惡意擴充套件。
差別只在燒的是誰的錢,還有出事時誰要負責說明。通常還是你。
BYOK 底下還有一整套託管、權限、儲存與撤銷問題,那不是今天的範圍。
如果供應商提供 OAuth 或短效權杖,應優先使用。今天只要記住:
具備付款或後端權限的長效 bearer API key,不該內嵌或持久化在前端。
公開鍵或可發布鍵不是同一類憑證。
一個後端代理前端不再拿著任何供應商金鑰。還有一份自己跑過的驗收紀錄,兩招都做了,不是憑印象覺得改好了。
如果你今天打開 Network,發現真實金鑰已經在外面躺了三個月,先別急著刪 repo,也不要只刪程式碼。
先到供應商後台撤銷或輪替那把金鑰,讓舊金鑰失效。後面我們會再完整處理使用紀錄、影響範圍與 repository 歷史;順序很重要,做錯會白忙一整天。
明天要問下一個問題:金鑰搬到伺服器的環境變數了,那裡就安全嗎?
.env 不是魔法盾牌。
明天見。